diff --git a/kpimtextedit/tests/textedittest.cpp b/kpimtextedit/tests/textedittest.cpp index e86ab5a48..cdcf228f4 100644 --- a/kpimtextedit/tests/textedittest.cpp +++ b/kpimtextedit/tests/textedittest.cpp @@ -1,324 +1,380 @@ /* Copyright (c) 2009 Thomas McGuire This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "qtest_kde.h" #include "textedittest.h" #include "../textedit.h" #include "kmime/kmime_codecs.h" #include #include #include #include using namespace KPIMTextEdit; QTEST_KDEMAIN( TextEditTester, GUI ) void TextEditTester::testFormattingUsed() { // This method tries to test everything that krichtextedit makes available, so // we can sure that in KMail, when the user uses some formatting, the mail is actually // sent as HTML mail TextEdit textEdit; QVERIFY( !textEdit.isFormattingUsed() ); // Insert some text. QTextCursor cursor( textEdit.document() ); cursor.insertText( QLatin1String( "Hello World!!" ) ); QVERIFY( !textEdit.isFormattingUsed() ); cursor.setPosition( 1 ); textEdit.setTextCursor( cursor ); // // Test link // QString someUrl = QLatin1String( "www.test.de" ); QString altText = QLatin1String( "Hello" ); textEdit.updateLink( someUrl, altText ); QVERIFY( textEdit.isFormattingUsed() ); QCOMPARE( textEdit.currentLinkUrl(), someUrl ); QCOMPARE( textEdit.currentLinkText(), altText ); cursor.setPosition( 1 ); textEdit.setTextCursor( cursor ); textEdit.updateLink( QString(), QLatin1String( "Hello" ) ); QVERIFY( textEdit.currentLinkUrl().isEmpty() ); QVERIFY( !textEdit.currentLinkText().isEmpty() ); QVERIFY( !textEdit.isFormattingUsed() ); // // Test alignment // cursor.setPosition( 1 ); textEdit.setTextCursor( cursor ); textEdit.alignRight(); QVERIFY( textEdit.isFormattingUsed() ); QCOMPARE( textEdit.alignment(), Qt::AlignRight ); textEdit.alignLeft(); QVERIFY( !textEdit.isFormattingUsed() ); textEdit.alignCenter(); QCOMPARE( textEdit.alignment(), Qt::AlignHCenter ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.alignJustify(); QCOMPARE( textEdit.alignment(), Qt::AlignJustify ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.alignLeft(); QCOMPARE( textEdit.alignment(), Qt::AlignLeft ); QVERIFY( !textEdit.isFormattingUsed() ); // // Test lists // textEdit.setListStyle( QTextListFormat::ListCircle ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setListStyle( 0 ); QVERIFY( !textEdit.isFormattingUsed() ); // // Test font attributes // textEdit.setFontFamily( QLatin1String( "Times" ) ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setFontFamily( textEdit.document()->defaultFont().family() ); QVERIFY( !textEdit.isFormattingUsed() ); textEdit.setFontSize( 48 ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setFontSize( textEdit.document()->defaultFont().pointSize() ); QVERIFY( !textEdit.isFormattingUsed() ); QFont myFont = textEdit.document()->defaultFont(); myFont.setStyle( QFont::StyleOblique ); textEdit.setFont( myFont ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setFont( textEdit.document()->defaultFont() ); QVERIFY( !textEdit.isFormattingUsed() ); // // Test bold, italic, underline and strikeout // textEdit.setTextBold( true ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setTextBold( false ); QVERIFY( !textEdit.isFormattingUsed() ); textEdit.setTextUnderline( true ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setTextUnderline( false ); QVERIFY( !textEdit.isFormattingUsed() ); textEdit.setTextItalic( true ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setTextItalic( false ); QVERIFY( !textEdit.isFormattingUsed() ); textEdit.setTextStrikeOut( true ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setTextStrikeOut( false ); QVERIFY( !textEdit.isFormattingUsed() ); // // Color // QColor oldForeground = textEdit.document()->firstBlock().charFormat().foreground().color(); textEdit.setTextForegroundColor( Qt::red ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setTextForegroundColor( oldForeground ); QVERIFY( !textEdit.isFormattingUsed() ); QColor oldBackground = textEdit.document()->firstBlock().charFormat().background().color(); textEdit.setTextBackgroundColor( Qt::red ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setTextBackgroundColor( oldBackground ); QVERIFY( !textEdit.isFormattingUsed() ); // // Horizontal rule // textEdit.insertHorizontalRule(); QVERIFY( textEdit.isFormattingUsed() ); // No way to easily remove the horizontal line, so clear the text edit and start over textEdit.clear(); cursor.insertText( QLatin1String( "Hello World!!" ) ); QVERIFY( !textEdit.isFormattingUsed() ); cursor.setPosition( 1 ); textEdit.setTextCursor( cursor ); // // Sub and superscript // textEdit.setTextSuperScript( true ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setTextSuperScript( false ); QVERIFY( !textEdit.isFormattingUsed() ); textEdit.setTextSubScript( true ); QVERIFY( textEdit.isFormattingUsed() ); textEdit.setTextSubScript( false ); QVERIFY( !textEdit.isFormattingUsed() ); // // Image // QString imagePath = KIconLoader::global()->iconPath( QLatin1String( "folder-new" ), KIconLoader::Small, false ); textEdit.addImage( imagePath ); QVERIFY( textEdit.isFormattingUsed() ); cursor = textEdit.textCursor(); cursor.movePosition( QTextCursor::Left, QTextCursor::KeepAnchor, 1 ); cursor.removeSelectedText(); QVERIFY( !textEdit.isFormattingUsed() ); } void TextEditTester::testQuoting() { TextEdit edit; QVERIFY( edit.isLineQuoted( QLatin1String( "> Hello" ) ) ); QVERIFY( edit.isLineQuoted( QLatin1String( ">Hello" ) ) ); QVERIFY( !edit.isLineQuoted( QLatin1String( "Hello" ) ) ); QCOMPARE( edit.quoteLength( QLatin1String( "Hello" ) ), 0 ); QCOMPARE( edit.quoteLength( QLatin1String( ">Hello" ) ), 1 ); QCOMPARE( edit.quoteLength( QLatin1String( "> Hello" ) ), 2 ); QCOMPARE( edit.quoteLength( QLatin1String( ">>>Hello" ) ), 3 ); QCOMPARE( edit.quoteLength( QLatin1String( "> > > Hello" ) ), 6 ); QCOMPARE( edit.quoteLength( QLatin1String( "|Hello" ) ), 1 ); QCOMPARE( edit.quoteLength( QLatin1String( "| |Hello" ) ), 3 ); } void TextEditTester::testCleanText() { TextEdit edit; QLatin1String html( "Heelllo World
Bye!" ); QLatin1String plain( "Heelllo World\nBye!" ); edit.setTextOrHtml( html ); edit.addImage( KIconLoader::global()->iconPath( QLatin1String( "folder-new" ), KIconLoader::Small, false ) ); QVERIFY( edit.textMode() == TextEdit::Rich ); QCOMPARE( edit.toCleanPlainText(), plain ); edit.show(); // < otherwise toWrappedPlainText can't work, it needs a layout QCOMPARE( edit.toWrappedPlainText(), plain ); } void TextEditTester::testEnter_data() { QTest::addColumn("initalText"); QTest::addColumn("expectedText"); QTest::addColumn("cursorPos"); QTest::newRow( "" ) << QString::fromAscii( "> Hello World" ) << QString::fromAscii( "> Hello \n> World" ) << 8; QTest::newRow( "" ) << QString::fromAscii( "Hello World" ) << QString::fromAscii( "Hello \nWorld" ) << 6; QTest::newRow( "" ) << QString::fromAscii( "> Hello World" ) << QString::fromAscii( "> Hello World\n" ) << 13; QTest::newRow( "" ) << QString::fromAscii( ">Hello World" ) << QString::fromAscii( ">Hello \n>World" ) << 7; QTest::newRow( "" ) << QString::fromAscii( "> > Hello World" ) << QString::fromAscii( "> > Hello \n> > World" ) << 10; QTest::newRow( "" ) << QString::fromAscii( "| | Hello World" ) << QString::fromAscii( "| | Hello \n| | World" ) << 10; } void TextEditTester::testEnter() { QFETCH( QString, initalText ); QFETCH( QString, expectedText ); QFETCH( int, cursorPos ); TextEdit edit; edit.setPlainText( initalText ); QTextCursor textCursor( edit.document() ); textCursor.setPosition( cursorPos ); edit.setTextCursor( textCursor ); QTest::keyClick( &edit, Qt::Key_Return ); QCOMPARE( edit.toPlainText(), expectedText ); } void TextEditTester::testImages() { TextEdit edit; QString image1Path = KIconLoader::global()->iconPath( QLatin1String( "folder-new" ), KIconLoader::Small, false ); QString image2Path = KIconLoader::global()->iconPath( QLatin1String( "arrow-up" ), KIconLoader::Small, false ); // Add one image, check that embeddedImages() returns the right stuff edit.addImage( image1Path ); KPIMTextEdit::ImageList images = edit.embeddedImages(); QCOMPARE( images.size(), 1 ); EmbeddedImage *image = images.first().data(); QCOMPARE( image->imageName, QString::fromAscii( "folder-new.png" ) ); // Also check that it loads the correct image QImage diskImage( image1Path ); QBuffer buffer; buffer.open( QIODevice::WriteOnly ); diskImage.save( &buffer, "PNG" ); QByteArray encodedImage = KMime::Codec::codecForName( "base64" )->encode( buffer.buffer() ); QCOMPARE( image->image, encodedImage ); // No image should be there after clearing edit.clear(); QVERIFY( edit.embeddedImages().isEmpty() ); // Check that manually removing the image also empties the image list edit.addImage( image1Path ); QCOMPARE( edit.embeddedImages().size(), 1 ); QTextCursor cursor = edit.textCursor(); cursor.setPosition( 0, QTextCursor::MoveAnchor ); cursor.movePosition( QTextCursor::Right, QTextCursor::KeepAnchor, 1 ); cursor.removeSelectedText(); QVERIFY( edit.embeddedImages().isEmpty() ); // Check that adding the identical image two times only adds the image once edit.addImage( image1Path ); edit.addImage( image1Path ); QCOMPARE( edit.embeddedImages().size(), 1 ); // Another different image added, and we should have two images edit.clear(); edit.addImage( image1Path ); edit.addImage( image2Path ); images = edit.embeddedImages(); QCOMPARE( images.size(), 2 ); KPIMTextEdit::EmbeddedImage *image1 = images.first().data(); KPIMTextEdit::EmbeddedImage *image2 = images.last().data(); QCOMPARE( image1->imageName, QString::fromAscii( "folder-new2.png" ) ); // ### FIXME: should be folder-new.png, but QTextEdit provides no way to remove cached resources! QCOMPARE( image2->imageName, QString::fromAscii( "arrow-up.png" ) ); QVERIFY( image1->contentID != image2->contentID ); } void TextEditTester::testImageHtmlCode() { TextEdit edit; QString image1Path = KIconLoader::global()->iconPath( QLatin1String( "folder-new" ), KIconLoader::Small, false ); QString image2Path = KIconLoader::global()->iconPath( QLatin1String( "arrow-up" ), KIconLoader::Small, false ); edit.addImage( image1Path ); edit.addImage( image2Path ); KPIMTextEdit::ImageList images = edit.embeddedImages(); QCOMPARE( images.size(), 2 ); KPIMTextEdit::EmbeddedImage *image1 = images.first().data(); KPIMTextEdit::EmbeddedImage *image2 = images.last().data(); QString startHtml = QLatin1String( "BlaBlub" ); QString endHtml = QString( QLatin1String( "BlaBlub" ) ) .arg( image2->contentID ).arg( image1->contentID ); QCOMPARE( TextEdit::imageNamesToContentIds( startHtml.toAscii(), images ), endHtml.toAscii() ); } + +void TextEditTester::testDeleteLine_data() +{ + QTest::addColumn("initalText"); + QTest::addColumn("expectedText"); + QTest::addColumn("cursorPos"); + + QTest::newRow( "" ) << QString::fromAscii( "line1\nline2\nline3" ) + << QString::fromAscii( "line1\nline3" ) + << 6; + QTest::newRow( "" ) << QString::fromAscii( "line1\nline2\nline3" ) + << QString::fromAscii( "line2\nline3" ) + << 5; + QTest::newRow( "" ) << QString::fromAscii( "line1\nline2\nline3" ) + << QString::fromAscii( "line1\nline3" ) + << 11; + QTest::newRow( "" ) << QString::fromAscii( "line1\nline2\nline3" ) + << QString::fromAscii( "line2\nline3" ) + << 0; + QTest::newRow( "" ) << QString::fromAscii( "line1\nline2\nline3" ) + << QString::fromAscii( "line1\nline2" ) + << 17; + QTest::newRow( "" ) << QString::fromAscii( "line1" ) + << QString::fromAscii( "" ) + << 0; + QTest::newRow( "" ) << QString::fromAscii( "line1" ) + << QString::fromAscii( "" ) + << 5; + + // Now, test deletion with word wrapping. The line with the Ms is so long that it will get wrapped + QTest::newRow( "" ) << QString::fromAscii( "line1\nMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\nline3" ) + << QString::fromAscii( "line1\nMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\nline3" ) + << 6; + QTest::newRow( "" ) << QString::fromAscii( "line1\nMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\nline3" ) + << QString::fromAscii( "line1\nMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\nline3" ) + << 13; +} + +void TextEditTester::testDeleteLine() +{ + QFETCH( QString, initalText ); + QFETCH( QString, expectedText ); + QFETCH( int, cursorPos ); + + TextEdit edit; + edit.setPlainText( initalText ); + QTextCursor cursor = edit.textCursor(); + cursor.setPosition( cursorPos ); + edit.setTextCursor( cursor ); + + edit.show(); // we need a layout for this to work + + edit.deleteCurrentLine(); + QCOMPARE( edit.toPlainText(), expectedText ); +} + diff --git a/kpimtextedit/tests/textedittest.h b/kpimtextedit/tests/textedittest.h index 523bfe4b8..dc7a86b90 100644 --- a/kpimtextedit/tests/textedittest.h +++ b/kpimtextedit/tests/textedittest.h @@ -1,39 +1,41 @@ /* Copyright (c) 2009 Thomas McGuire This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef TEXTEDITTEST_H #define TEXTEDITTEST_H #include class TextEditTester : public QObject { Q_OBJECT private slots: void testFormattingUsed(); void testQuoting(); void testCleanText(); void testEnter(); void testEnter_data(); void testImages(); void testImageHtmlCode(); + void testDeleteLine(); + void testDeleteLine_data(); }; #endif diff --git a/kpimtextedit/textedit.cpp b/kpimtextedit/textedit.cpp index ede74e94f..5101fab7d 100644 --- a/kpimtextedit/textedit.cpp +++ b/kpimtextedit/textedit.cpp @@ -1,607 +1,666 @@ /* Copyright (c) 2009 Thomas McGuire Based on KMail and libkdepim code by: Copyright 2007 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "textedit.h" #include "emailquotehighlighter.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace KPIMTextEdit { class TextEditPrivate { public: TextEditPrivate( TextEdit *parent ) - : q( parent ), + : actionAddImage( 0 ), + actionDeleteLine( 0 ), + q( parent ), imageSupportEnabled( false ) { } /** * Helper function for addImage(), which does the actual work of adding the QImage as a * resource to the document, pasting it and adding it to the image name list. * * @param imageName the desired image name. If it is already taken, a number will * be appended to it * @param image the actual image to add */ void addImageHelper( const QString &imageName, const QImage &image ); /** * Helper function to get the list of all QTextImageFormats in the document. */ QList embeddedImageFormats() const; /** * Removes embedded image markers, converts non-breaking spaces to normal spaces * and other fixes for strings that came from toPlainText()/toHtml(). */ void fixupTextEditString( QString &text ) const; /** * Does the constructor work */ void init(); /** * Opens a file dialog to let the user choose an image and then pastes that * image to the editor */ void _k_slotAddImage(); + void _k_slotDeleteLine(); + /// The action that triggers _k_slotAddImage() KAction *actionAddImage; + /// The action that triggers _k_slotDeleteLine() + KAction *actionDeleteLine; + /// The parent class TextEdit *q; /// Whether or not adding or pasting images is supported bool imageSupportEnabled; /** * The names of embedded images. * Used to easily obtain the names of the images. * New images are compared to the the list and not added as resource if already present. */ QStringList mImageNames; /** * Although KTextEdit keeps track of the spell checking state, we override * it here, because we have a highlighter which does quote highlighting. * And since disabling spellchecking in KTextEdit simply would turn off our * quote highlighter, we never actually deactivate spell checking in the * base class, but only tell our own email highlighter to not highlight * spelling mistakes. * For this, we use the KTextEditSpellInterface, which is basically a hack * that makes it possible to have our own enabled/disabled state in a binary * compatible way. */ bool spellCheckingEnabled; }; } // namespace using namespace KPIMTextEdit; void TextEditPrivate::fixupTextEditString( QString &text ) const { // Remove line separators. Normal \n chars are still there, so no linebreaks get lost here text.remove( QChar::LineSeparator ); // Get rid of embedded images, see QTextImageFormat documentation: // "Inline images are represented by an object replacement character (0xFFFC in Unicode) " text.remove( 0xFFFC ); // In plaintext mode, each space is non-breaking. text.replace( QChar::Nbsp, QChar::fromAscii( ' ' ) ); } TextEdit::TextEdit( const QString& text, QWidget *parent ) : KRichTextWidget( text, parent ), d( new TextEditPrivate( this ) ) { d->init(); } TextEdit::TextEdit( QWidget *parent ) : KRichTextWidget( parent ), d( new TextEditPrivate( this ) ) { d->init(); } TextEdit::~TextEdit() { } bool TextEdit::eventFilter( QObject*o, QEvent* e ) { if ( o == this ) KCursor::autoHideEventFilter( o, e ); return KRichTextWidget::eventFilter( o, e ); } void TextEditPrivate::init() { q->setSpellInterface( q ); // We tell the KRichTextWidget to enable spell checking, because only then it will // call createHighlighter() which will create our own highlighter which also // does quote highlighting. // However, *our* spellchecking is still disabled. Our own highlighter only // cares about our spellcheck status, it will not highlight missspelled words // if our spellchecking is disabled. // See also KEMailQuotingHighlighter::highlightBlock(). spellCheckingEnabled = false; q->setCheckSpellingEnabledInternal( true ); KCursor::setAutoHideCursor( q, true, true ); q->installEventFilter( q ); } void TextEdit::keyPressEvent ( QKeyEvent * e ) { if ( e->key() == Qt::Key_Return ) { QTextCursor cursor = textCursor(); int oldPos = cursor.position(); int blockPos = cursor.block().position(); //selection all the line. cursor.movePosition( QTextCursor::StartOfBlock ); cursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor ); QString lineText = cursor.selectedText(); if ( ( ( oldPos -blockPos ) > 0 ) && ( ( oldPos-blockPos ) < int( lineText.length() ) ) ) { bool isQuotedLine = false; int bot = 0; // bot = begin of text after quote indicators while ( bot < lineText.length() ) { if( ( lineText[bot] == QChar::fromAscii( '>' ) ) || ( lineText[bot] == QChar::fromAscii( '|' ) ) ) { isQuotedLine = true; ++bot; } else if ( lineText[bot].isSpace() ) { ++bot; } else { break; } } KRichTextWidget::keyPressEvent( e ); // duplicate quote indicators of the previous line before the new // line if the line actually contained text (apart from the quote // indicators) and the cursor is behind the quote indicators if ( isQuotedLine && ( bot != lineText.length() ) && ( ( oldPos-blockPos ) >= int( bot ) ) ) { // The cursor position might have changed unpredictably if there was selected // text which got replaced by a new line, so we query it again: cursor.movePosition( QTextCursor::StartOfBlock ); cursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor ); QString newLine = cursor.selectedText(); // remove leading white space from the new line and instead // add the quote indicators of the previous line int leadingWhiteSpaceCount = 0; while ( ( leadingWhiteSpaceCount < newLine.length() ) && newLine[leadingWhiteSpaceCount].isSpace() ) { ++leadingWhiteSpaceCount; } newLine = newLine.replace( 0, leadingWhiteSpaceCount, lineText.left( bot ) ); cursor.insertText( newLine ); //cursor.setPosition( cursor.position() + 2); cursor.movePosition( QTextCursor::StartOfBlock ); setTextCursor( cursor ); } } else KRichTextWidget::keyPressEvent( e ); } else { KRichTextWidget::keyPressEvent( e ); } } bool TextEdit::isSpellCheckingEnabled() const { return d->spellCheckingEnabled; } void TextEdit::setSpellCheckingEnabled( bool enable ) { EMailQuoteHighlighter *hlighter = dynamic_cast( highlighter() ); if ( hlighter ) hlighter->toggleSpellHighlighting( enable ); d->spellCheckingEnabled = enable; emit checkSpellingChanged( enable ); } bool TextEdit::shouldBlockBeSpellChecked( const QString& block ) const { return !isLineQuoted( block ); } bool KPIMTextEdit::TextEdit::isLineQuoted( const QString& line ) const { return quoteLength( line ) > 0; } int KPIMTextEdit::TextEdit::quoteLength( const QString& line ) const { bool quoteFound = false; int startOfText = -1; for ( int i = 0; i < line.length(); i++ ) { if ( line[i] == QLatin1Char( '>' ) || line[i] == QLatin1Char( '|' ) ) quoteFound = true; else if ( line[i] != QLatin1Char( ' ' ) ) { startOfText = i; break; } } if ( quoteFound ) { if ( startOfText == -1 ) startOfText = line.length() - 1; return startOfText; } else return 0; } const QString KPIMTextEdit::TextEdit::defaultQuoteSign() const { return QLatin1String( "> " ); } void TextEdit::createHighlighter() { EMailQuoteHighlighter *emailHighLighter = new EMailQuoteHighlighter( this ); setHighlighterColors( emailHighLighter ); //TODO change config KRichTextWidget::setHighlighter( emailHighLighter ); if ( !spellCheckingLanguage().isEmpty() ) setSpellCheckingLanguage( spellCheckingLanguage() ); setSpellCheckingEnabled( isSpellCheckingEnabled() ); } void TextEdit::setHighlighterColors( EMailQuoteHighlighter *highlighter ) { Q_UNUSED( highlighter ); } QString TextEdit::toWrappedPlainText() const { QString temp; QTextDocument* doc = document(); QTextBlock block = doc->begin(); while ( block.isValid() ) { QTextLayout* layout = block.layout(); for ( int i = 0; i < layout->lineCount(); i++ ) { QTextLine line = layout->lineAt( i ); temp += block.text().mid( line.textStart(), line.textLength() ) + QLatin1Char( '\n' ); } block = block.next(); } // Remove the last superfluous newline added above if ( temp.endsWith( QLatin1Char( '\n' ) ) ) temp.chop( 1 ); d->fixupTextEditString( temp ); return temp; } QString TextEdit::toCleanPlainText() const { QString temp = toPlainText(); d->fixupTextEditString( temp ); return temp; } void TextEdit::createActions( KActionCollection *actionCollection ) { KRichTextWidget::createActions( actionCollection ); if ( d->imageSupportEnabled ) { d->actionAddImage = new KAction( KIcon( QLatin1String( "insert-image" ) ), i18n( "Add Image" ), this ); actionCollection->addAction( QLatin1String( "add_image" ), d->actionAddImage ); connect( d->actionAddImage, SIGNAL(triggered(bool) ), SLOT( _k_slotAddImage() ) ); } + + d->actionDeleteLine = new KAction( i18n( "Delete Line" ), this ); + d->actionDeleteLine->setShortcut( QKeySequence( Qt::CTRL + Qt::Key_K ) ); + actionCollection->addAction( QLatin1String( "delete_line" ), d->actionDeleteLine ); + connect( d->actionDeleteLine, SIGNAL(triggered(bool)), SLOT(_k_slotDeleteLine()) ); } void TextEdit::addImage( const KUrl &url ) { QImage image; if ( !image.load( url.path() ) ) { KMessageBox::error( this, i18nc( "@info", "Unable to load image %1.", url.path() ) ); return; } QFileInfo fi( url.path() ); QString imageName = fi.baseName().isEmpty() ? QLatin1String( "image.png" ) : fi.baseName() + QLatin1String( ".png" ); d->addImageHelper( imageName, image ); } void TextEditPrivate::addImageHelper( const QString &imageName, const QImage &image ) { QString imageNameToAdd = imageName; QTextDocument *document = q->document(); // determine the imageNameToAdd int imageNumber = 1; while ( mImageNames.contains( imageNameToAdd ) ) { QVariant qv = document->resource( QTextDocument::ImageResource, QUrl( imageNameToAdd ) ); if ( qv == image ) { // use the same name break; } int firstDot = imageName.indexOf( QLatin1Char( '.' ) ); if ( firstDot == -1 ) imageNameToAdd = imageName + QString::number( imageNumber++ ); else imageNameToAdd = imageName.left( firstDot ) + QString::number( imageNumber++ ) + imageName.mid( firstDot ); } if ( !mImageNames.contains( imageNameToAdd ) ) { document->addResource( QTextDocument::ImageResource, QUrl( imageNameToAdd ), image ); mImageNames << imageNameToAdd; } q->textCursor().insertImage( imageNameToAdd ); q->enableRichTextMode(); } QList< QSharedPointer > TextEdit::embeddedImages() const { QList< QSharedPointer > retImages; QStringList seenImageNames; QList imageFormats = d->embeddedImageFormats(); foreach( const QTextImageFormat &imageFormat, imageFormats ) { if ( !seenImageNames.contains( imageFormat.name() ) ) { QVariant data = document()->resource( QTextDocument::ImageResource, QUrl( imageFormat.name() ) ); QImage image = qvariant_cast( data ); QBuffer buffer; buffer.open( QIODevice::WriteOnly ); image.save( &buffer, "PNG" ); qsrand( QDateTime::currentDateTime().toTime_t() + qHash( imageFormat.name() ) ); QSharedPointer embeddedImage( new EmbeddedImage() ); retImages.append( embeddedImage ); embeddedImage->image = KMime::Codec::codecForName( "base64" )->encode( buffer.buffer() ); embeddedImage->imageName = imageFormat.name(); embeddedImage->contentID = QString( QLatin1String( "%1" ) ).arg( qrand() ); seenImageNames.append( imageFormat.name() ); } } return retImages; } QList TextEditPrivate::embeddedImageFormats() const { QTextDocument *doc = q->document(); QList retList; QTextBlock currentBlock = doc->begin(); while ( currentBlock.isValid() ) { QTextBlock::iterator it; for ( it = currentBlock.begin(); !it.atEnd(); ++it ) { QTextFragment fragment = it.fragment(); if ( fragment.isValid() ) { QTextImageFormat imageFormat = fragment.charFormat().toImageFormat(); if ( imageFormat.isValid() ) { retList.append( imageFormat ); } } } currentBlock = currentBlock.next(); } return retList; } void TextEditPrivate::_k_slotAddImage() { QPointer fdlg = new KFileDialog( QString(), QString(), q ); fdlg->setOperationMode( KFileDialog::Other ); fdlg->setCaption( i18n("Add Image") ); fdlg->okButton()->setGuiItem( KGuiItem( i18n("&Add"), QLatin1String( "document-open" ) ) ); fdlg->setMode( KFile::Files ); if ( fdlg->exec() != KDialog::Accepted ) { delete fdlg; return; } const KUrl::List files = fdlg->selectedUrls(); foreach ( const KUrl& url, files ) { q->addImage( url ); } delete fdlg; } void KPIMTextEdit::TextEdit::enableImageActions() { d->imageSupportEnabled = true; } QByteArray KPIMTextEdit::TextEdit::imageNamesToContentIds( const QByteArray &htmlBody, const KPIMTextEdit::ImageList &imageList ) { QByteArray result = htmlBody; if ( imageList.size() > 0 ) { foreach( const QSharedPointer &image, imageList ) { const QString newImageName = QLatin1String( "cid:" ) + image->contentID; QByteArray quote( "\"" ); result.replace( QByteArray( quote + image->imageName.toLocal8Bit() + quote ), QByteArray( quote + newImageName.toLocal8Bit() + quote ) ); } } return result; } void TextEdit::insertFromMimeData( const QMimeData *source ) { // Add an image if that is on the clipboard if ( textMode() == KRichTextEdit::Rich && source->hasImage() && d->imageSupportEnabled ) { QImage image = qvariant_cast( source->imageData() ); QFileInfo fi( source->text() ); QString imageName = fi.baseName().isEmpty() ? i18nc( "Start of the filename for an image", "image" ) : fi.baseName(); d->addImageHelper( imageName, image ); return; } // Attempt to paste HTML contents into the text edit in plain text mode, // prevent this and prevent plain text instead. if ( textMode() == KRichTextEdit::Plain && source->hasHtml() ) { if ( source->hasText() ) { insertPlainText( source->text() ); return; } } KRichTextWidget::insertFromMimeData( source ); } bool KPIMTextEdit::TextEdit::canInsertFromMimeData( const QMimeData *source ) const { if ( source->hasHtml() && textMode() == KRichTextEdit::Rich ) return true; if ( source->hasText() ) return true; if ( textMode() == KRichTextEdit::Rich && source->hasImage() && d->imageSupportEnabled ) return true; return KRichTextWidget::canInsertFromMimeData( source ); } static bool isCharFormatFormatted( const QTextCharFormat &format, const QFont &defaultFont, const QTextCharFormat &defaultBlockFormat ) { if ( !format.anchorHref().isEmpty() || format.font() != defaultFont || format.isAnchor() || format.verticalAlignment() != defaultBlockFormat.verticalAlignment() || format.underlineStyle() != defaultBlockFormat.underlineStyle() || format.foreground().color() != defaultBlockFormat.foreground().color() || format.background().color() != defaultBlockFormat.background().color() ) return true; return false; } static bool isBlockFormatFormatted( const QTextBlockFormat &format, const QTextBlockFormat &defaultFormat ) { if ( format.alignment() != defaultFormat.alignment() || format.indent() != defaultFormat.indent() || format.textIndent() != defaultFormat.textIndent() ) return true; return false; } /// @return true if the format represents a list, table, image or something like that. static bool isSpecial( const QTextFormat &charFormat ) { return charFormat.isFrameFormat() || charFormat.isImageFormat() || charFormat.isListFormat() || charFormat.isTableFormat(); } bool TextEdit::isFormattingUsed() const { if ( textMode() == Plain ) return false; // Below, we walk through all text blocks and through all text fragments in them // and check if any of those has any formatting. // To check if they have formatting, we use the functions isBlockFormatFormatted() and // isCharFormatFormatted(). Those do not check all the exising formatting possibilities on // earth, but everything that KRichTextEdit supports at the moment. // // Also, we have to compare the formats against those of a default text edit. For example, // we can't compare the foreground color against black, because the user might have another // color scheme. Therefore we compare the foreground color against a default text edit. QTextEdit defaultTextEdit; QTextCharFormat defaultCharFormat = defaultTextEdit.document()->begin().charFormat(); QTextBlockFormat defaultBlockFormat = defaultTextEdit.document()->begin().blockFormat(); QFont defaultFont = document()->defaultFont(); QTextBlock block = document()->firstBlock(); while ( block.isValid() ) { if ( isBlockFormatFormatted( block.blockFormat(), defaultBlockFormat ) ) { return true; } if ( isSpecial( block.charFormat() ) || isSpecial( block.blockFormat() ) || block.textList() ) { return true; } QTextBlock::iterator it = block.begin(); while ( !it.atEnd() ) { QTextFragment fragment = it.fragment(); QTextCharFormat charFormat = fragment.charFormat(); if ( isSpecial( charFormat ) ) { return true; } if ( isCharFormatFormatted( fragment.charFormat(), defaultFont, defaultCharFormat ) ) { return true; } it++; } block = block.next(); } if ( toHtml().contains( QLatin1String( "
" ) ) ) return true; return false; } +void TextEditPrivate::_k_slotDeleteLine() +{ + q->deleteCurrentLine(); +} + +void TextEdit::deleteCurrentLine() +{ + QTextCursor cursor = textCursor(); + QTextBlock block = cursor.block(); + const QTextLayout* layout = block.layout(); + + // The current text block can have several lines due to word wrapping. + // Search the line the cursor is in, and then delete it. + for ( int lineNumber = 0; lineNumber < layout->lineCount(); lineNumber++ ) { + QTextLine line = layout->lineAt( lineNumber ); + const bool lastLineInBlock = ( line.textStart() + line.textLength() == block.length() - 1 ); + const bool oneLineBlock = ( layout->lineCount() == 1 ); + const int startOfLine = block.position() + line.textStart(); + int endOfLine = block.position() + line.textStart() + line.textLength(); + if ( !lastLineInBlock ) + endOfLine -= 1; + + // Found the line where the cursor is in + if ( cursor.position() >= startOfLine && cursor.position() <= endOfLine ) { + int deleteStart = startOfLine; + int deleteLength = line.textLength(); + if ( oneLineBlock ) + deleteLength++; // The trailing newline + + // When deleting the last line in the document, + // remove the newline of the line before the last line instead + if ( deleteStart + deleteLength >= document()->characterCount() && + deleteStart > 0 ) + deleteStart--; + + cursor.beginEditBlock(); + cursor.setPosition( deleteStart ); + cursor.movePosition( QTextCursor::NextCharacter, QTextCursor::KeepAnchor, deleteLength ); + cursor.removeSelectedText(); + cursor.endEditBlock(); + return; + } + } + +} + + #include "textedit.moc" diff --git a/kpimtextedit/textedit.h b/kpimtextedit/textedit.h index 5fccddc34..5358e689a 100644 --- a/kpimtextedit/textedit.h +++ b/kpimtextedit/textedit.h @@ -1,263 +1,271 @@ /* Copyright (c) 2009 Thomas McGuire Based on KMail and libkdepim code by: Copyright 2007 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KPIMTEXTEDIT_TEXTEDIT_H #define KPIMTEXTEDIT_TEXTEDIT_H #include "kpimtextedit_export.h" #include #include #include #include class KUrl; namespace KPIMTextEdit { class TextEditPrivate; class EMailQuoteHighlighter; /** * Holds information about an embedded HTML image. * A list with all images can be retrieved with TextEdit::embeddedImages(). */ struct EmbeddedImage { QByteArray image; ///< The image, encoded as PNG with base64 encoding QString contentID; ///< The content id of the embedded image QString imageName; ///< Name of the image as it is available as a resource in the editor }; typedef QList< QSharedPointer > ImageList; /** * Special textedit that provides additional features which are useful for PIM applications * like mail clients. * Additional features this class provides: * - Highlighting quoted text * - Handling of inline images * - Auto-Hiding the cursor * - Handling of pastes and drops of images * * @since 4.3 */ class KPIMTEXTEDIT_EXPORT TextEdit : public KRichTextWidget, protected KTextEditSpellInterface // TODO: KDE5: get rid of the spell interface { Q_OBJECT public: /** * Constructs a TextEdit object * @param text the initial plain text of the text edit, interpreted as HTML * @param parent the parent widget */ explicit TextEdit( const QString& text, QWidget *parent = 0 ); /** * Constructs a TextEdit object. * @param parent the parent widget */ explicit TextEdit( QWidget *parent = 0 ); /** * Calling this allows createActions() to create the add image actions. * Call this method before callilng createActions(), otherwise the action * will not be added. * Also, if image actions is enabled, the user can paste PNG images. * * Don't call this if you don't want to support adding images. */ void enableImageActions(); /** * Destructor */ ~TextEdit(); /** * Reimplemented from KMEditor, to support more actions. * * The additional action XML names are: * - add_image + * - delete_line * * The add_image actions is only added if enableImageActions() is called before. */ virtual void createActions( KActionCollection *actionCollection ); /** * Adds an image. The image is loaded from file and then pasted to the current * cursor position. * * @param url The URL of the file which contains the image */ void addImage( const KUrl &url ); + /** + * Deletes the line at the current cursor position. + * @since 4.4 + */ + void deleteCurrentLine(); + /** * Get a list with all embedded HTML images. * If the same image is contained twice or more in the editor, it will have only * one entry in this list. * * @return a list of embedded HTML images of the editor. */ ImageList embeddedImages() const; /** * Returns the text of the editor as plain text, with linebreaks inserted * where word-wrapping occurred. */ QString toWrappedPlainText() const; /** * Same as toPlainText() from QTextEdit, only that it removes embedded images and * converts non-breaking space characters to normal spaces. */ QString toCleanPlainText() const; /** * This method is called after the highlighter is created. * If you use custom colors for highlighting, override this method and set the colors * to the highlighter in it. * * The default implementation does nothing, therefore the default colors of the * EMailQuoteHighlighter class will be used. * * @param highlighter the highlighter that was just created. You need to set the colors * of this highlighter. */ virtual void setHighlighterColors( EMailQuoteHighlighter *highlighter ); /** * Convenience method for qouteLength( line ) > 0 */ bool isLineQuoted( const QString &line ) const; /** * This is called whenever the editor needs to find out the length of the quote, * i.e. the length of the quote prefix before the real text starts. * The default implementation counts the number of spaces, '>' and '|' chars in * front of the line. * * @param line the line of which the length of the quote prefix should be returned * @return 0 if the line is not quoted, the length of the quote prefix otherwise * FIXME: Not yet used in all places, e.g. keypressEvent() or the quote highlighter */ virtual int quoteLength( const QString &line ) const; /** * Returns the prefix that is added to a line that is quoted. * By default, this is "> ". */ virtual const QString defaultQuoteSign() const; /** * For all given embedded images, this function replace the image name in the tag of the * HTML body with cid:content-id, * so that the HTML references the image body parts, see RFC 2557. * * This is useful when building a MIME message with inline images. * * Note that this function works on encoded content already. * * @param htmlBody the HTML code in which the tag will be modified. * The HTML code here could come from toHtml(), for example. * * @param imageList the list of images of which the tag will be modified. * You can get such a list from the embeddedImages() function. * * @return a modified HTML code, where the tags got replaced */ static QByteArray imageNamesToContentIds( const QByteArray &htmlBody, const ImageList &imageList ); /** * Checks if rich text formatting is used anywhere. * This is not the same as checking whether textMode() returns "Rich", since * that only tells that rich text mode is enabled, but not if any special formatting * is actually used. * * @return true if formatting is used anywhere */ bool isFormattingUsed() const; protected: /** * Reimplemented for inline image support */ virtual bool canInsertFromMimeData( const QMimeData *source ) const; /** * Reimplemented for inline image support */ virtual void insertFromMimeData( const QMimeData *source ); /** * Reimplemented from KRichTextWidget to hide the mouse cursor when there * was no mouse movement for some time, using KCursor */ virtual bool eventFilter( QObject*o, QEvent* e ); /** * Reimplemented to add qoute signs when the user presses enter * on a quoted line. */ virtual void keyPressEvent ( QKeyEvent * e ); // For the explaination for these four methods, see the comment at the // spellCheckingEnabled variable of the private class. /** * Reimplemented from KTextEditSpellInterface */ virtual bool isSpellCheckingEnabled() const; /** * Reimplemented from KTextEditSpellInterface */ virtual void setSpellCheckingEnabled( bool enable ); /** * Reimplemented from KTextEditSpellInterface, to avoid spellchecking * quoted text. */ virtual bool shouldBlockBeSpellChecked( const QString& block ) const; /** * Reimplemented to create our own highlighter which does quote and * spellcheck highlighting */ virtual void createHighlighter(); private: std::auto_ptr const d; friend class TextEditPrivate; Q_PRIVATE_SLOT( d, void _k_slotAddImage() ) + Q_PRIVATE_SLOT( d, void _k_slotDeleteLine() ) }; } // namespace #endif